package chloroform.oauth

import java.util.{Date, Locale}
import scala.util.Sorting
import java.net.{URL, URLEncoder}
import java.nio.charset.Charset
import java.io.InputStream
import util.crypto.{RSASHA1Signer, RSAEncoder, Nonce}
import util.StreamUtils
import util.Base64
import util.logging.Logged
import java.lang.{StringBuilder, String}

/**
 * Created by IntelliJ IDEA.
 * User: zslajchrt
 * Date: 27.2.11
 * Time: 21:13
 * To change this template use File | Settings | File Templates.
 */

@deprecated
object OAuthSession {
  val googleSite: String = "www.google.com"
  val OAuthGetRequestTokenURL: String = "https://www.google.com/accounts/OAuthGetRequestToken"
  val OAuthGetAccessTokenURL: String = "https://www.google.com/accounts/OAuthGetAccessToken"
  val oauth_consumer_key_param: String = "oauth_consumer_key"
  val oauth_signature_method_param: String = "oauth_signature_method"
  val oauth_signature_param: String = "oauth_signature"
  val oauth_timestamp_param: String = "oauth_timestamp"
  val oauth_nonce_param: String = "oauth_nonce"
  val oauth_version_param: String = "oauth_version"
  val oauth_callback_param: String = "oauth_callback"
  val xoauth_displayname_param: String = "xoauth_displayname"
  val oauth_token_param: String = "oauth_token"
  val oauth_token_secret_param: String = "oauth_token_secret"
  val oauth_verifier_param: String = "oauth_verifier"
  val oauth_callback_confirmed_param: String = "oauth_callback_confirmed"
  val scope_param: String = "scope"

  def signatureBaseString(method: String, baseUrl: String, params: Array[(String, String)]): String = {
    val builder = new StringBuilder
    builder.append(method).append('&').append(encodeUrl(baseUrl)).append('&')

    // sort the parameters by their names
    implicit def pair2ordered(pair: Tuple2[String, String]) = new Ordered[Tuple2[String, String]] {
      def compare(that: (String, String)) = pair._1.compareTo(that._1)
    }
    Sorting.quickSort(params)

    val paramBuilder = new StringBuilder
    // merge each pair to the string conforming to the "key=value" form
    val keyValuePairs: Array[String] = params.map(pair => pair._1 + '=' + pair._2)
    paramBuilder.append(keyValuePairs.head)
    // separate the tail pairs by &
    keyValuePairs.tail.foreach(kv => {
      paramBuilder.append('&').append(kv)
    })

    builder.append(encodeUrl(paramBuilder.toString))

    builder.toString
  }


  def parsePairs(source: String): Map[String, String] = {
    var pairMap = Map.empty[String, String]
    val splitSource: Array[String] = source.split('&')
    splitSource.foreach(pairSource => {
      val splitPair: Array[String] = pairSource.split('=')
      require(splitPair.length == 2, "Pair " + pairSource + " contains more '=' characters")
      pairMap += (splitPair(0) -> splitPair(1))
    })

    pairMap
  }

  def constructAuthorizationRequestPath(token: String, hostedDomain: String, language: Locale, mobile: Boolean): String = {
    val authReqBuilder = new StringBuilder
    // It is not necessary to encode oauth_token as it already holds a URL-encoded value
    authReqBuilder.append("/accounts/OAuthAuthorizeToken?oauth_token=").append(token)
    if (hostedDomain != null) {
      authReqBuilder.append("&hd=").append(encodeUrl(hostedDomain))
    } else {
      authReqBuilder.append("&hd=default")
    }
    if (language != null) {
      authReqBuilder.append("&hl=").append(language.getLanguage)
    }
    if (mobile) {
      authReqBuilder.append("&btmpl=mobile")
    }
    authReqBuilder.toString
  }

  def encodeUrl(s: String) = URLEncoder.encode(s, "US-ASCII")

}

@deprecated
class HttpRequest(val method: String, val path: String, val host: String, val ssl: Boolean, val headers: List[(String, String)] = Nil, val body: String = null, val httpVersion: String = "1.1") {
  val query = method + " " + path + " HTTP/" + httpVersion
}

@deprecated
trait HttpResponse {
  def responseCode() : Int
  def header(name: String) : String
  def body() : InputStream
}

@deprecated
trait HttpRequestExecutor {
  def execute(request: HttpRequest): HttpResponse
}

@deprecated
sealed abstract case class OAuthStatus
@deprecated
case object OAuthInitialized extends OAuthStatus
@deprecated
case object OAuthHasRequestToken extends OAuthStatus
@deprecated
case object OAuthHasAccessToken extends OAuthStatus

@serializable
@SerialVersionUID(1)
@deprecated
abstract class OAuthSession(oauth_consumer_key: String, @transient privateKeyStream: InputStream, scopes: List[String],
                            oauth_callback: String, hostedDomain: String = null, language: Locale = null,
                            mobile: Boolean = false, oauth_version: String = "1.0",
                            xoauth_displayname: String = null) extends HttpRequestExecutor with Logged {

  private[this] val scope: String = {
    require(!scopes.isEmpty, "no scopes requested")

    val builder = new StringBuilder
    builder.append(scopes.head)
    // separate the scopes in the tail by the space
    scopes.tail.foreach(s => {
      builder.append(' ').append(s)
    })
    builder.toString
  }

  private[this] var status: OAuthStatus = OAuthInitialized
  def getStatus() = status

  private val encodedKey: Array[Byte] = RSASHA1Signer.read(privateKeyStream)
  @transient private[this] var sig: RSASHA1Signer = null
  def signer: RSASHA1Signer = if (sig == null) {
    sig = new RSASHA1Signer(encodedKey)
    sig
  } else sig

  /**
   * URL encoded request or access token. It is double-encoded in the signature.
   */
  private[oauth] var oauth_token: String = null
  /**
   * URL encoded token secret. It does not appear in the signature.
   */
  private[this] var oauth_token_secret: String = null
  private[this] var oauth_callback_confirmed: Boolean = false
  /**
   * URL encoded verifier. It is double-encoded in the signature.
   */
  private[oauth] var oauth_verifier: String = null
  private[this] var oauth_signature_method: String = "RSA-SHA1"

  private[this] def oauth_timestamp() = ("" + new Date().getTime).substring(0, 10)


  @transient private[this] var nonceGenerator: Nonce = null
  def nonceGen = if (nonceGenerator == null) {
    nonceGenerator = new Nonce(16)
    nonceGenerator
  } else nonceGenerator

  private def encodeUrl(s: String) = OAuthSession.encodeUrl(s)

  private[oauth] def signatureBaseForGetRequestToken(nonce: String, timestamp: String): String = {

    var params = List(
      (OAuthSession.oauth_consumer_key_param, encodeUrl(oauth_consumer_key)),
      (OAuthSession.oauth_signature_method_param, oauth_signature_method),
      (OAuthSession.oauth_timestamp_param, timestamp),
      (OAuthSession.oauth_nonce_param, nonce),
      (OAuthSession.oauth_version_param, oauth_version),
      (OAuthSession.oauth_callback_param, encodeUrl(oauth_callback)),
      (OAuthSession.scope_param, encodeUrl(scope))
    )
    if (xoauth_displayname != null)
      params ::= (OAuthSession.xoauth_displayname_param, encodeUrl(xoauth_displayname))

    val paramsAsArray = params.toArray

    OAuthSession.signatureBaseString("GET", OAuthSession.OAuthGetRequestTokenURL, paramsAsArray)
  }

  private[oauth] def signatureBaseForGetAccessToken(nonce: String, timestamp: String): String = {

    val params = Array(
      (OAuthSession.oauth_consumer_key_param, encodeUrl(oauth_consumer_key)),
      (OAuthSession.oauth_nonce_param, nonce),
      (OAuthSession.oauth_signature_method_param, oauth_signature_method),
      (OAuthSession.oauth_timestamp_param, timestamp),
      (OAuthSession.oauth_token_param, oauth_token), // already url-encoded
      (OAuthSession.oauth_verifier_param, oauth_verifier), // already url-encoded
      (OAuthSession.oauth_version_param, oauth_version)
    )
    OAuthSession.signatureBaseString("GET", OAuthSession.OAuthGetAccessTokenURL, params)
  }

  private[oauth] def queryForGetRequestToken = "/accounts/OAuthGetRequestToken?scope=" + encodeUrl(scope)

  private[oauth] def queryForGetAccessToken = "/accounts/OAuthGetAccessToken"

  private[oauth] def authorizationHeaderForGetRequestToken(nonce: String, timestamp: String, signature: String): String = {
    val encryptedSignature = Base64.encodeBytes(signer.sign(signature))

    val builder = new StringBuilder
    builder.append("OAuth ").
      append(OAuthSession.oauth_version_param).append("=\"").append(oauth_version).append("\", ").
      append(OAuthSession.oauth_nonce_param).append("=\"").append(nonce).append("\", ").
      append(OAuthSession.oauth_timestamp_param).append("=\"").append(timestamp).append("\", ").
      append(OAuthSession.oauth_consumer_key_param).append("=\"").append(encodeUrl(oauth_consumer_key)).append("\", ").
      append(OAuthSession.oauth_callback_param).append("=\"").append(encodeUrl(oauth_callback)).append("\", ").
      append(OAuthSession.oauth_signature_method_param).append("=\"").append(oauth_signature_method).append("\", ").
      append(OAuthSession.oauth_signature_param).append("=\"").append(encodeUrl(encryptedSignature)).append("\"")

    if (xoauth_displayname != null)
      builder.append(", ").append(OAuthSession.xoauth_displayname_param).append("=\"").append(encodeUrl(xoauth_displayname)).append("\"")

    builder.toString
  }

  private[oauth] def authorizationHeaderForGetAccessToken(nonce: String, timestamp: String, signature: String): String = {
    val encryptedSignature = Base64.encodeBytes(signer.sign(signature))

    val builder = new StringBuilder
    builder.append("OAuth ").
      append(OAuthSession.oauth_version_param).append("=\"").append(oauth_version).append("\", ").
      append(OAuthSession.oauth_nonce_param).append("=\"").append(nonce).append("\", ").
      append(OAuthSession.oauth_timestamp_param).append("=\"").append(timestamp).append("\", ").
      append(OAuthSession.oauth_consumer_key_param).append("=\"").append(encodeUrl(oauth_consumer_key)).append("\", ").
      // the verifier is already URL-encoded
      append(OAuthSession.oauth_verifier_param).append("=\"").append(oauth_verifier).append("\", ").
      // the request token is already URL-encoded
      append(OAuthSession.oauth_token_param).append("=\"").append(oauth_token).append("\", ").
      append(OAuthSession.oauth_signature_method_param).append("=\"").append(oauth_signature_method).append("\", ").
      append(OAuthSession.oauth_signature_param).append("=\"").append(encodeUrl(encryptedSignature)).append("\"")

    builder.toString
  }

  private[oauth] def authorizationRequest(nonce: String, timestamp: String): HttpRequest = {
    val signature = signatureBaseForGetRequestToken(nonce, timestamp)
    val authHeader = authorizationHeaderForGetRequestToken(nonce, timestamp, signature)

    executeTokenRequest(queryForGetRequestToken, authHeader)

    val authReqPath = OAuthSession.constructAuthorizationRequestPath(oauth_token, hostedDomain, language, mobile)

    status = OAuthHasRequestToken

    new HttpRequest("GET", authReqPath, OAuthSession.googleSite, true)
  }

  // API

  def authorizationRequest(): HttpRequest = {
    val nonce = nonceGen.generateNonce
    val timestamp: String = oauth_timestamp
    authorizationRequest(nonce, timestamp)
  }

  /**
   * This method executes the request for either a request or an access token and updates both
   * the <code>oauth_token</code> and <code>oauth_token_secret</code> fields as long as the execution succeeds.
   */
  protected def executeTokenRequest(query: String, authHeader: String) {
    val request = new HttpRequest("GET", query, OAuthSession.googleSite, true,
      List(("Accept", "*/*"),
        ("Authorization", authHeader)))

    debug("Request token request sent: " + query)
    val response = execute(request)
    debug("Request token response received with reply code " + response.responseCode)

    if (response.responseCode == 200) {
      // ok
      val responseBody = StreamUtils.readLine(response.body)
      val pairs = OAuthSession.parsePairs(responseBody)

      pairs.get(OAuthSession.oauth_token_param) match {
        case None => throw new IllegalStateException("oauth_token is missing in the response")
        case Some(token) => oauth_token = token // the request token
      }

      debug("Request token: " + oauth_token)

      pairs.get(OAuthSession.oauth_token_secret_param) match {
        case None => warn("oauth_token_secret is missing in the response")
        case Some(secret) => oauth_token_secret = secret
      }

    } else throw new IllegalStateException("Request for a request token failed. Code " + response.responseCode)
  }

  /**
   * @parameter auth_token
   * @parameter auth_verifier
   */
  def handleAuthorizationResponse(token: String, verifier: String) {
    require(encodeUrl(token) == this.oauth_token, "The request token from the redirect is not same as the current request token.")

    this.oauth_verifier = encodeUrl(verifier)

    val nonce = nonceGen.generateNonce
    val timestamp: String = oauth_timestamp
    val signature = signatureBaseForGetAccessToken(nonce, timestamp)
    val authHeader = authorizationHeaderForGetAccessToken(nonce, timestamp, signature)

    executeTokenRequest(queryForGetAccessToken, authHeader)

    // Now, the oauth_token should contain the access token.
    status = OAuthHasAccessToken
  }

}